/**
 *    Copyright (C) 2011 Nadim Benabdenbi <nadim.benabdenbi@gmail.com>
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.sun.codemodel;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import com.sun.codemodel.util.ClassNameComparator;

/**
 * overrides of original class by adding {@link #listThrowables()}
 */
public class JMethod extends JGenerifiableImpl implements JDeclaration, JAnnotatable, JDocCommentable {
	
	/**
	 * Modifiers for this method
	 */
	private final JMods mods;
	
	/**
	 * Return type for this method
	 */
	private JType type = null;
	
	/**
	 * Name of this method
	 */
	private String name = null;
	
	/**
	 * List of parameters for this method's declaration
	 */
	private final List<JVar> params = new ArrayList<JVar>();
	
	/**
	 * Set of exceptions that this method may throw. A set instance lazily created.
	 */
	private Set<JClass> _throws;
	
	/**
	 * JBlock of statements that makes up the body this method
	 */
	private JBlock body = null;
	
	private final JDefinedClass outer;
	
	/**
	 * javadoc comments for this JMethod
	 */
	private JDocComment jdoc = null;
	
	/**
	 * Variable parameter for this method's varargs declaration introduced in J2SE 1.5
	 */
	private JVar varParam = null;
	
	/**
	 * Annotations on this variable. Lazily created.
	 */
	private List<JAnnotationUse> annotations = null;
	
	private boolean isConstructor() {
		return type == null;
	}
	
	/**
	 * To set the default value for the annotation member
	 */
	private JExpression defaultValue = null;
	
	/**
	 * JMethod constructor
	 * 
	 * @param mods
	 *            Modifiers for this method's declaration
	 * 
	 * @param type
	 *            Return type for the method
	 * 
	 * @param name
	 *            Name of this method
	 */
	JMethod(JDefinedClass outer, int mods, JType type, String name) {
		this.mods = JMods.forMethod(mods);
		this.type = type;
		this.name = name;
		this.outer = outer;
	}
	
	/**
	 * Constructor constructor
	 * 
	 * @param mods
	 *            Modifiers for this constructor's declaration
	 * 
	 * @param _class
	 *            JClass containing this constructor
	 */
	JMethod(int mods, JDefinedClass _class) {
		this.mods = JMods.forMethod(mods);
		this.type = null;
		this.name = _class.name();
		this.outer = _class;
	}
	
	private Set<JClass> getThrows() {
		if (_throws == null)
			_throws = new TreeSet<JClass>(ClassNameComparator.theInstance);
		return _throws;
	}
	
	/**
	 * Add an exception to the list of exceptions that this method may throw.
	 * 
	 * @param exception
	 *            Name of an exception that this method may throw
	 */
	public JMethod _throws(JClass exception) {
		getThrows().add(exception);
		return this;
	}
	
	public JMethod _throws(Class<? extends Throwable> exception) {
		return _throws(outer.owner().ref(exception));
	}
	
	/**
	 * Add the specified variable to the list of parameters for this method signature.
	 * 
	 * @param type
	 *            JType of the parameter being added
	 * 
	 * @param name
	 *            Name of the parameter being added
	 * 
	 * @return New parameter variable
	 */
	public JVar param(int mods, JType type, String name) {
		JVar v = new JVar(JMods.forVar(mods), type, name, null);
		params.add(v);
		return v;
	}
	
	public JVar param(JType type, String name) {
		return param(JMod.NONE, type, name);
	}
	
	public JVar param(int mods, Class<?> type, String name) {
		return param(mods, outer.owner()._ref(type), name);
	}
	
	public JVar param(Class<?> type, String name) {
		return param(outer.owner()._ref(type), name);
	}
	
	/**
	 * @see #varParam(JType, String)
	 */
	public JVar varParam(Class<?> type, String name) {
		return varParam(outer.owner()._ref(type), name);
	}
	
	/**
	 * Add the specified variable argument to the list of parameters for this method signature.
	 * 
	 * @param type
	 *            Type of the parameter being added.
	 * 
	 * @param name
	 *            Name of the parameter being added
	 * 
	 * @return the variable parameter
	 * 
	 * @throws IllegalStateException
	 *             If this method is called twice. varargs in J2SE 1.5 can appear only once in the method signature.
	 */
	public JVar varParam(JType type, String name) {
		if (!hasVarArgs()) {
			
			varParam = new JVar(JMods.forVar(JMod.NONE), type.array(), name, null);
			return varParam;
		} else {
			throw new IllegalStateException("Cannot have two varargs in a method,\n" + "Check if varParam method of JMethod is" + " invoked more than once");
			
		}
		
	}
	
	/**
	 * Adds an annotation to this variable.
	 * 
	 * @param clazz
	 *            The annotation class to annotate the field with
	 */
	public JAnnotationUse annotate(JClass clazz) {
		if (annotations == null)
			annotations = new ArrayList<JAnnotationUse>();
		JAnnotationUse a = new JAnnotationUse(clazz);
		annotations.add(a);
		return a;
	}
	
	/**
	 * Adds an annotation to this variable.
	 * 
	 * @param clazz
	 *            The annotation class to annotate the field with
	 */
	public JAnnotationUse annotate(Class<? extends Annotation> clazz) {
		return annotate(owner().ref(clazz));
	}
	
	public <W extends JAnnotationWriter> W annotate2(Class<W> clazz) {
		return TypedAnnotationWriter.create(clazz, this);
	}
	
	public Collection<JAnnotationUse> annotations() {
		if (annotations == null)
			annotations = new ArrayList<JAnnotationUse>();
		return Collections.unmodifiableList(annotations);
	}
	
	/**
	 * Check if there are any varargs declared for this method signature.
	 */
	public boolean hasVarArgs() {
		return this.varParam != null;
	}
	
	public String name() {
		return name;
	}
	
	/**
	 * Changes the name of the method.
	 */
	public void name(String n) {
		this.name = n;
	}
	
	/**
	 * Returns the return type.
	 */
	public JType type() {
		return type;
	}
	
	/**
	 * Overrides the return type.
	 */
	public void type(JType t) {
		this.type = t;
	}
	
	/**
	 * Returns all the parameter types in an array.
	 * 
	 * @return If there's no parameter, an empty array will be returned.
	 */
	public JType[] listParamTypes() {
		JType[] r = new JType[params.size()];
		for (int i = 0; i < r.length; i++)
			r[i] = params.get(i).type();
		return r;
	}
	
	/**
	 * Returns the varags parameter type.
	 * 
	 * @return If there's no vararg parameter type, null will be returned.
	 */
	public JType listVarParamType() {
		if (varParam != null)
			return varParam.type();
		else
			return null;
	}
	
	/**
	 * Returns all the parameters in an array.
	 * 
	 * @return If there's no parameter, an empty array will be returned.
	 */
	public JVar[] listParams() {
		return params.toArray(new JVar[params.size()]);
	}
	
	/**
	 * Returns the variable parameter
	 * 
	 * @return If there's no parameter, null will be returned.
	 */
	public JVar listVarParam() {
		return varParam;
	}
	
	/**
	 * Returns true if the method has the specified signature.
	 */
	public boolean hasSignature(JType[] argTypes) {
		JVar[] p = listParams();
		if (p.length != argTypes.length)
			return false;
		
		for (int i = 0; i < p.length; i++)
			if (!p[i].type().equals(argTypes[i]))
				return false;
		
		return true;
	}
	
	/**
	 * Get the block that makes up body of this method
	 * 
	 * @return Body of method
	 */
	public JBlock body() {
		if (body == null)
			body = new JBlock();
		return body;
	}
	
	/**
	 * Specify the default value for this annotation member
	 * 
	 * @param value
	 *            Default value for the annotation member
	 * 
	 */
	public void declareDefaultValue(JExpression value) {
		this.defaultValue = value;
	}
	
	/**
	 * Creates, if necessary, and returns the class javadoc for this JDefinedClass
	 * 
	 * @return JDocComment containing javadocs for this class
	 */
	public JDocComment javadoc() {
		if (jdoc == null)
			jdoc = new JDocComment(owner());
		return jdoc;
	}
	
	@Override
	public void declare(JFormatter f) {
		if (jdoc != null)
			f.g(jdoc);
		
		if (annotations != null) {
			for (JAnnotationUse a : annotations)
				f.g(a).nl();
		}
		
		f.g(mods);
		
		// declare the generics parameters
		super.declare(f);
		
		if (!isConstructor())
			f.g(type);
		f.id(name).p('(').i();
		// when parameters are printed in new lines, we want them to be indented.
		// there's a good chance no newlines happen, too, but just in case it does.
		boolean first = true;
		for (JVar var : params) {
			if (!first)
				f.p(',');
			if (var.isAnnotated())
				f.nl();
			f.b(var);
			first = false;
		}
		if (hasVarArgs()) {
			if (!first)
				f.p(',');
			f.g(varParam.type().elementType());
			f.p("... ");
			f.id(varParam.name());
		}
		
		f.o().p(')');
		if (_throws != null && !_throws.isEmpty()) {
			f.nl().i().p("throws").g(_throws).nl().o();
		}
		
		if (defaultValue != null) {
			f.p("default ");
			f.g(defaultValue);
		}
		if (body != null) {
			f.s(body);
		} else if (!outer.isInterface() && !outer.isAnnotationTypeDeclaration() && !mods.isAbstract() && !mods.isNative()) {
			// Print an empty body for non-native, non-abstract methods
			f.s(new JBlock());
		} else {
			f.p(';').nl();
		}
	}
	
	/**
	 * @return the current modifiers of this method. Always return non-null valid object.
	 */
	public JMods mods() {
		return mods;
	}
	
	/**
	 * @deprecated use {@link #mods()}
	 */
	@Deprecated
	public JMods getMods() {
		return mods;
	}
	
	@Override
	protected JCodeModel owner() {
		return outer.owner();
	}
	
	public List<JClass> listThrowables() {
		List<JClass> exceptions = new ArrayList<JClass>();
		Set<JClass> _throws = getThrows();
		for (JClass exception : _throws) {
			exceptions.add(exception);
		}
		return exceptions;
	}
}
